<?php


namespace App\Common\Libs\Ali;

use App\Common\Libs\Dispatch\Console;
use App\Console\Commands\Controller\LibraryFileCommand;
use App\Console\Commands\Controller\UploadCommand;
use App\Consts\ExceptionCode;
use App\Consts\GlobalConsoleConst;
use App\Consts\GlobalConst;
use App\Consts\GlobalUploadConst;
use App\Exceptions\Error;
use App\Models\LibraryFile;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use OSS\Model\ObjectInfo;
use OSS\Model\PrefixInfo;

/**
 * OSS处理类
 * @time 2020.09.13
 * @author dumpling
 * @package common\libs
 */
class Oss
{

    /**
     * @var $_oss
     */
    private static $_oss;

    /**
     * @var $_ossBucket
     */
    private $_ossBucket;

    /**
     * @var $_ossObject
     */
    private $_ossObject;

    /**
     * @var OssClient
     */
    private $_ossApp;

    /**
     * @var \Aliyun\OSS\OSSClient
     */
    private $_ossClient;

    /**
     * 要上传到OSS的路径
     * @var $_ossObjectPath
     */
    private $_ossObjectPath;

    /**
     * 文件保存的临时目录，默认时common/downloads/
     * @var $_ossTmpDir
     */
    private $_ossTmpDir;

    /**
     * 文件来源
     * @var $_from
     */
    private $_from;

    /**
     * 入{sys_file_library}表时，所需的类型，具体可看SysFileLibrary.php
     * @var $_fileType
     */
    private $_fileType;

    /**
     * 入{sys_file_library}表时，对应fileType的ID
     * @var $_targetId
     */
    private $_targetId;

    /**
     * @var $_instance
     */
    private static $_instance;

    /**
     * 入口
     * @param string $objectPath [OSS路径]
     * @param int $fileType [入{sys_file_library}表时，所需的类型，具体可看SysFileLibrary.php]
     * @param int $from [来自，0前台，1后台]
     * @param string $targetId [入{sys_file_library}表时，对应fileType的ID]
     * @param string $filename [指定文件名称]
     * @return Oss
     */
    public static function getInstance(string $objectPath, int $fileType = GlobalUploadConst::TYPE_ARTICLE, int $from = GlobalUploadConst::FROM_BACKEND, string $targetId = '', string $filename = ''): self
    {
        $objectPath = $objectPath ?: '/';
        if (!isset(self::$_instance[$objectPath])) {
            self::$_oss = new self();
            self::$_instance[$objectPath] = self::$_oss;
        }
        $aliApp = OssClient::getInstance();
        self::$_oss->_ossApp = $aliApp;
        self::$_oss->_ossClient = $aliApp->client;
        self::$_oss->_ossBucket = $aliApp->bucket;
        self::$_oss->_ossObject = $objectPath . $filename;
        self::$_oss->_ossObjectPath = $objectPath;
        self::$_oss->_ossTmpDir = storage_path('downloads/');

        self::$_oss->_from = $from;
        self::$_oss->_fileType = $fileType;
        self::$_oss->_targetId = $targetId;

        return self::$_oss;
    }

    /**
     * 将远程图片上传到OSS
     * @param string $remoteUrl
     * @param string|int $userId
     * @param string $filename 指定oss上文件名【源文件名的hash值】
     * @param array $replaceResources
     * @param bool $isForce
     * @return bool|false|string
     */
    public function upload($remoteUrl, $userId = '', $filename = '', $replaceResources = [], $isForce = false)
    {
        if (false === $isForce && strpos($remoteUrl, 'github') !== false) {
            Console::getInstance(UploadCommand::class)
                ->push([
                    'target_id' => $replaceResources['target_id'],
                    'target_type' => $replaceResources['target_type'],
                    'url' => $remoteUrl,
                    'type' => $replaceResources['type'],
                ], GlobalConsoleConst::UPLOAD_LOG_REPLACE_RESOURCES);
            return false;
        }
        // 将远程图片下载到本地
        $localFileInfo = RemoteFile::getInstance($remoteUrl, $filename)->downloadToLocal();
        $md5File = md5_file($localFileInfo['path'] . $localFileInfo['filename']);

        // 准备上传oss所需的object
        $this->_ossObject = $this->_ossObjectPath . $localFileInfo['filename'];
        //验证图片是否存在oss 和图片库，存在直接返回ossUrl，不存在上传oss
        $ossUrl = $this->judgeFileExist($localFileInfo['path'] . $localFileInfo['filename'], $md5File);
        if (!$ossUrl) {
            // 上传文件到OSS
            $ossUrl = $this->toOss($localFileInfo['path'] . $localFileInfo['filename']);
            // 将文件异步保存到[library_file]表中
            $this->saveLibraryFile([
                'user_id' => $userId,
                'type' => $this->_fileType,
                'target_id' => $this->_targetId,
                'file_hash' => $md5File,
                'url' => $ossUrl,
                'original' => $remoteUrl,//原文件地址
                'size' => $localFileInfo['size'],
                'postfix' => $localFileInfo['postfix'],
                'file_mime' => $localFileInfo['file_mime'],
                'from' => $this->_from
            ]);
        }
        return $ossUrl;
    }

    /**
     * @param string $filePath
     * @param string $objectPath
     * @param bool $isDel
     * @return bool|false|string
     */
    public function uploadLocalFile(string $filePath, $objectPath = '', bool $isDel = true)
    {
        if ($objectPath) {
            $this->_ossObject = $objectPath . substr($filePath, strrpos($filePath, '/') + 1);
        }
        return $this->toOss($filePath, false, $isDel);
    }

    /**
     * 上传客户端文件到OSS
     * @param bool $isByte 上传类型 true：文件形式上传，false：文件流形式上传
     * @param bool $isDel
     * @return bool|false|string
     */
    public function uploadClientFile(bool $isByte = false, bool $isDel = true)
    {
        // 接收上传的文件
        $file = request()->file('file');
        if (empty($file) || !$file->isValid()) {
            return false;
        }
        return $this->clientFileToOss($file, $isByte, $isDel);
    }

    /**
     * @title 批量上传客户端文件到OSS
     * @param string $userId
     * @param bool $uploadType 上传类型 true：文件形式上传，false：文件流形式上传
     * @param bool $isDel
     * @return bool|false|array|int
     */
    public function batchUploadClientFile(string $userId, bool $isByte = false, bool $isDel = true)
    {
        // 接收上传的文件
        $files = request()->file('file');
        //记录上传日志
        if (!$files) {
            return false;
        }
        $ossUrls = [];
        $height = '';//图片高度
        $width = '';//图片宽度
        foreach ($files as $file) {
            //解析源文件的高度和宽度 图片格式 ：604430abd04b4cea0d2f27961203a2a7.jpeg?width=100,height=500
            $urlQuery = explode('?', $file->getClientOriginalName());
            if (!empty($urlQuery[1])) {
                $imgQueryWidth = substr($urlQuery[1], 0, strripos($urlQuery[1], ",") + 1);
                $imgQueryHeight = substr($urlQuery[1], strripos($urlQuery[1], ",") + 1);
                $imgQueryWidth = explode('=', $imgQueryWidth);
                $imgQueryHeight = explode('=', $imgQueryHeight);
                if (!empty($imgQueryWidth[1])) {
                    $width = trim($imgQueryWidth[1], ',');
                }
                if (!empty($imgQueryHeight[1])) {
                    $height = trim($imgQueryHeight[1], ',');
                }
            }
            // 获取文件拓展名【注意：这里不能用 $file->getExtension()】
            $extension = substr($file->getClientMimeType(), strrpos($file->getClientMimeType(), '/') + 1);

            // 如果是svg的图片，mime类型是image/svg+xml
            if (strpos($extension, 'svg') !== false) {
                $extension = substr($extension, 0, strrpos($extension, '+'));
            }
            // 将文件到保存到本地，并获取hash值
            $filename = md5(Str::random(32) . time()) . '.' . $extension;
            $filePath = $this->_ossTmpDir . $filename;
            Storage::disk('oss')->put('downloads/' . $filename, file_get_contents($file->getRealPath()));
            $md5File = md5_file($filePath);
            $this->_ossObject = $this->_ossObjectPath . $filename;
            //验证图片是否存在oss 和图片库，存在直接返回ossUrl，不存在上传oss
            $ossUrl = $this->judgeFileExist($filePath, $md5File);
            if (!$ossUrl) {
                // 上传文件到OSS
                $ossUrl = $this->toOss($filePath, $isByte, $isDel);
                if (empty($ossUrl)) {
                    //图片上传失败
                    return ExceptionCode::FILE_UPLOAD_FAILED;
                }
                // 将文件异步保存到[library_file]表中
                $this->saveLibraryFile([
                    'user_id' => $userId,
                    'type' => $this->_fileType,
                    'target_id' => $this->_targetId,
                    'file_hash' => $md5File,
                    'url' => $ossUrl,
                    'original' => $file->getClientOriginalName(),
                    'size' => $file->getSize(),
                    'postfix' => $extension,
                    'file_mime' => $file->getType(),
                    'from' => $this->_from
                ]);
            }
            $ossUrls[] = [
                'url' => $ossUrl,
                'width' => $width,
                'height' => $height,
            ];
        }
        return $ossUrls;
    }

    /**
     * @title 批量上传客户端文件到OSS
     * @param string $userId
     * @param bool $uploadType 上传类型 true：文件形式上传，false：文件流形式上传
     * @param bool $isDel
     * @return bool|false|array|int
     */
    public function batchUploadClientFileByBase64(string $userId, bool $isByte = false, bool $isDel = true)
    {
        // 接收上传的文件
        $base64Arr = request()->input('file');

        $ossUrls = [];
        foreach ($base64Arr as $base64Str) {
            preg_match('/^(data:\s*image\/(\w+);base64,)/', $base64Str, $res);
            $extension = $res[2];

            // 将文件到保存到本地
            $filename = md5(Str::random(32) . time()) . '.' . $extension;
            $filePath = $this->_ossTmpDir . $filename;
            $base64Img = base64_decode(str_replace($res[1], '', $base64Str));

            Storage::disk('oss')->put('downloads/' . $filename, $base64Img);

            $this->_ossObject = $this->_ossObjectPath . $filename;

            // 上传文件到OSS
            $ossUrl = $this->toOss($filePath, $isByte, $isDel);
            if (empty($ossUrl)) {
                return false;
            }

            $ossUrls[] = [
                'url' => $ossUrl,
            ];
        }
        return $ossUrls;
    }

    /**
     * @param $file \Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]|array|null
     * @param bool $isByte 上传类型 true：文件形式上传，false：文件流形式上传
     * @param $isDel
     * @return bool|false|string
     */
    private function clientFileToOss($file, $isByte, $isDel)
    {
        // 获取文件拓展名
        $extension = $file->getClientOriginalExtension();
        // 如果是svg的图片，mime类型是image/svg+xml
        if (strpos($extension, 'svg') !== false) {
            $extension = substr($extension, 0, strrpos($extension, '+'));
        }
        // 将文件到保存到本地，并获取hash值
        $filename = md5(Str::random(32) . time()) . '.' . $extension;
        $filePath = $this->_ossTmpDir . $filename;
        Storage::disk('oss')->put('downloads/' . $filename, file_get_contents($file->getRealPath()));
        $md5File = md5_file($filePath);
        $this->_ossObject = $this->_ossObjectPath . $filename;
        //验证图片是否存在oss 和图片库，存在直接返回ossUrl，不存在上传oss
        $ossUrl = $this->judgeFileExist($filePath, $md5File);
        if (!$ossUrl) {
            // 上传文件到OSS
            $ossUrl = $this->toOss($filePath, $isByte, $isDel);
            // 将文件异步保存到[sys_file_library]表中
            $this->saveLibraryFile([
                'type' => $this->_fileType,
                'target_id' => $this->_targetId,
                'file_hash' => $md5File,
                'url' => $ossUrl,
                'original' => $file->getClientOriginalName(),
                'size' => $file->getSize(),
                'postfix' => $extension,
                'file_mime' => $file->getType(),
                'from' => $this->_from
            ]);
        }
        return $ossUrl;
    }

    /**
     * 上传【项目资源，如：文章】信息到OSS
     * @param $target
     * @param bool $isDel
     * @param string $filename
     * @return bool|false|string
     */
    public function uploadTarget($target, $isDel = true, $filename = '')
    {
        $target = collect($target)->toArray();
        $target_data = json_encode($target, JSON_UNESCAPED_UNICODE);
        if (!$filename) {
            $targetId = $target['id'];
            $filename = $targetId . '.json';
        }
        $filePath = $this->_ossTmpDir . $filename;
        !is_dir($this->_ossTmpDir) && mkdir($this->_ossTmpDir, 0777, true);
        $fp = fopen($this->_ossTmpDir . $filename, 'w+');
        fwrite($fp, $target_data);
        fclose($fp);
        $this->_ossObject = $this->_ossObjectPath . $filename;
        return $this->toOss($filePath, false, $isDel);
    }

    /**
     * 根据文件名称，删除OSS文件
     * @param string|null $objectPath
     * @param string|null $filename
     * @return bool
     */
    public function deleteOssFileByFilename(string $objectPath = null, string $filename = null): bool
    {
        if ($objectPath && $filename) {
            $this->_ossObject = $objectPath . $filename;
        }
        try {
            $this->_ossClient->deleteObject([
                'Bucket' => $this->_ossBucket,
                'Key' => $this->_ossObject
            ]);
            return true;
        } catch (\Exception $e) {
            Log::channel('oss')->error('[deleteOssFileByFilename] - ' . $e->getMessage());
            return false;
        }
    }

    /**
     * 获取文件信息
     *
     * @param string|null $objectPath
     * @param string|null $filename
     * @return bool|string|array
     */
    public function getObject(string $filename = null, string $objectPath = null)
    {
        if ($objectPath) {
            $this->_ossObjectPath = $objectPath;
        }
        if ($filename) {
            $this->_ossObject = $this->_ossObjectPath . $filename;
        }
        try {
            return $this->_ossApp->getObject($this->_ossObject, $this->_ossBucket);
        } catch (\Exception $e) {
            Log::channel('oss')->error('[getObject] - ' . $e->getMessage());
            //$message = str_replace(PHP_EOL, ' ^M', $e->getMessage()) . ' or server time error. ';
            //return Error::handle('', $e->getCode(), $message, true);
            return false;
        }
    }

    /**
     * 验证OSS文件是否存在
     *
     * @param string|null $objectPath
     * @param string|null $filename
     * @return bool
     */
    public function judgeOssFileExist(string $objectPath = null, string $filename = null)
    {
        if ($objectPath && $filename) {
            $this->_ossObject = $objectPath . $filename;
        }
        try {
            return $this->_ossClient->getObject([
                'Bucket' => $this->_ossBucket,
                'Key' => $this->_ossObject
            ]) ? true : false;
        } catch (\Exception $e) {
            Log::channel('oss')->error('[judgeOssFileExist] - ' . $e->getMessage());
            return false;
        }
    }

    /**
     * @param string $dir
     * @param int $maxKey
     * @param string $delimiter
     * @param string $nextMarker
     * @return array|bool
     */
    public function fileList($dir = '', $maxKey = 30, $delimiter = '/', $nextMarker = '')
    {
        $dir = $dir ?: $this->_ossObjectPath;
        $fileList = []; // 获取的文件列表, 数组的一阶表示分页结果
        $dirList = []; // 获取的目录列表, 数组的一阶表示分页结果
        $storageList = [
            'file' => [], // 真正的文件数组
            'dir' => [], // 真正的目录数组
        ];
        while (true) {
            $options = [
                'Delimiter' => $delimiter,
                'Prefix' => $dir,
                'MaxKeys' => $maxKey,
                'Marker' => $nextMarker,
            ];
            try {
                $fileListInfo = $this->_ossClient->listObjects(array_merge($options, [
                    'Bucket' => $this->_ossBucket
                ]));
                // 得到nextMarker, 从上一次 listObjects 读到的最后一个文件的下一个文件开始继续获取文件列表, 类似分页
            } catch (\Exception $e) {
                Log::channel('oss')->error('[fileList] - ' . $e->getMessage());
                return false;
            }
            $nextMarker = $fileListInfo->getNextMarker();
            $fileItem = $fileListInfo->getObjectSummarys();
            $dirItem = $fileListInfo->getCommonPrefixes();
            $fileList[] = $fileItem;
            $dirList[] = $dirItem;
            if ($nextMarker === '') break;
        }
        foreach ($fileList[0] as $item) {
            $objectInfo = $this->objectInfoParse($item);
            if ($objectInfo['name'] != $dir) {
                $storageList['file'][] = $objectInfo;
            }
        }
        foreach ($dirList[0] as $item) {
            $storageList['dir'][] = $this->prefixInfoParse($item);
        }
        return $storageList;
    }

    /**
     * @param string $dir
     * @param int $maxKey
     * @param string $delimiter
     * @param string $nextMarker
     * @param bool $havePrefix
     * @return array
     */
    public function getAllFilenamesList($dir = '', $maxKey = 30, $delimiter = '/', $nextMarker = '', $havePrefix = false)
    {
        $dir = $dir ?: $this->_ossObjectPath;
        $filenames = [];
        $fileListInfo = $this->getFileListInfo($dir, $maxKey, $delimiter, $nextMarker);
        $nextMarker = $fileListInfo->getNextMarker();
        $fileList = $fileListInfo->getObjectSummarys();
        foreach ($fileList as $item) {
            $objectInfo = $this->objectInfoParse($item);
            if ($objectInfo['name'] != $dir) {
                $filenames[] = $havePrefix ? $objectInfo['name'] : substr($objectInfo['name'], strrpos($objectInfo['name'], '/') + 1);
            }
        }
        $nexFilenames = [];
        if ($nextMarker) {
            $nexFilenames = $this->getAllFilenamesList($dir, $maxKey, $delimiter, $nextMarker, $havePrefix);
        }
        return array_merge($filenames, $nexFilenames);
    }

    /**
     * 获取文件列表信息
     * 可以得到nextMarker, 从上一次 listObjects 读到的最后一个文件的下一个文件开始继续获取文件列表, 类似分页
     * @param string $dir
     * @param int $maxKey
     * @param string $delimiter
     * @param string $nextMarker
     * @return \Aliyun\OSS\Models\ObjectListing|bool
     */
    private function getFileListInfo($dir = '', $maxKey = 30, $delimiter = '/', $nextMarker = '')
    {
        $dir = $dir ?: $this->_ossObjectPath;
        $options = [
            'Delimiter' => $delimiter,
            'Prefix' => $dir,
            'MaxKeys' => $maxKey,
            'Marker' => $nextMarker,
        ];
        try {
            return $this->_ossClient->listObjects(array_merge($options, [
                'Bucket' => $this->_ossBucket
            ]));
        } catch (\Exception $e) {
            Log::channel('oss')->error('[getFileListInfo] - ' . $e->getMessage());
            return false;
        }
    }

    /**
     * 解析 prefixInfo 类
     * @param PrefixInfo $prefixInfo
     * @return array
     */
    private function prefixInfoParse(PrefixInfo $prefixInfo)
    {
        return [
            'dir' => $prefixInfo->getPrefix(),
        ];
    }

    /**
     * 解析 objectInfo 类
     * @param ObjectInfo $objectInfo
     * @return array
     */
    public function objectInfoParse(ObjectInfo $objectInfo)
    {
        return [
            'name' => $objectInfo->getKey(),
            'size' => $objectInfo->getSize(),
            'update_at' => $objectInfo->getLastModified(),
        ];
    }

    /**
     * @param $data
     * @return bool
     */
    private function saveLibraryFile($data)
    {
        Console::getInstance(LibraryFileCommand::class)
            ->push($data, GlobalConsoleConst::LIBRARY_FILE_CREATE);

        return true;
    }

    /**
     * 上传文件到OSS
     *
     * @param $filePath
     * @param bool $isByte 上传类型 true：文件形式上传，false：文件流形式上传
     * @param bool $isDel
     * @return bool|false|string
     */
    private function toOss($filePath, $isByte = false, $isDel = true)
    {
        try {
            // 上传文件
            $ossInfo = $this->_ossApp->putObject($this->_ossObject, $filePath, $isByte, $this->_ossBucket);
            // 如果是webp格式的文件，要利用python脚本进行转化
            if (strpos($ossInfo['url'], '.webp') !== false) {
                $filename = substr($filePath, strrpos($filePath, '/') + 1);
                $pyPath = dirname(__DIR__, 4) . '/script/webp2jpg.py';
                $filePath = storage_path('app/downloads/') . $filename;
                exec(GlobalConst::EXEC_PYTHON . " $pyPath $filePath $filename $this->_ossTmpDir 2>&1", $html, $res);
                Log::channel('spider')->info(__CLASS__ . " : " . __LINE__ . ' - ' . json_encode([
                        'params' => ['python_path' => $pyPath, 'file_path' => $filePath, 'file_name' => $filename, 'tmp_dir' => $this->_ossTmpDir],
                        'result' => $html,
                    ]));
                // 删除oss 和 本地的文件
                $res = $this->deleteOssFileByFilename();
                if ($isDel && file_exists($filePath)) {
                    @unlink($filePath);
                }
                // 重新赋值文件名称，并重新上传oss
                $filename = substr($filename, 0, strpos($filename, '.')) . '.jpg';
                $this->_ossObject = $this->_ossObjectPath . $filename;
                return $this->toOss($this->_ossTmpDir . $filename);
            }
            if ($isDel && file_exists($filePath)) {
                @unlink($filePath);
            }
            return basename($ossInfo['url']);
        } catch (\Exception $e) {
            Log::channel('oss')->error('[toOss] - ' . $e->getMessage());
            $message = str_replace(PHP_EOL, ' ^M', $e->getMessage()) . ' or server time error. ';
            return Error::handle('', $e->getCode(), $message, true);
        }
    }

    /**
     * 验证要上传的文件是否上传过
     * @param string $fileHash 文件hash值
     * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|object
     */
    private function judgeLocalFileExist($fileHash)
    {
        return LibraryFile::where([
            'file_hash' => $fileHash,
            'is_deleted' => GlobalConst::IS_NOT_DELETE,
        ])->first();
    }

    /**
     * @title 验证要上传的文件是否上传过
     * @param string $filePath 文件名
     * @param string $fileHash 文件hash值
     * @param bool $isDel
     * @return bool|mixed
     */
    private function judgeFileExist($filePath, $fileHash, $isDel = true)
    {
        // 检验[library_file]表中是否有记录，且未删除，状态正常
        $fileLibrary = $this->judgeLocalFileExist($fileHash);
        if ($fileLibrary) {
            // 检测OSS文件是否存在，若存在，直接返回
            if ($this->judgeOssFileExist($this->_ossObjectPath, $fileLibrary->url)) {
                if ($isDel && file_exists($filePath)) {
                    unlink($filePath);
                }
                return $fileLibrary->url;
            }
            $this->_ossObject = $this->_ossObjectPath . $fileLibrary->url;
        }
        return false;
    }

}
